<?php
/**
 * This class provides a simple interface for OpenID (1.1 and 2.0) authentication.
 * Supports Yadis discovery.
 * The autentication process is stateless/dumb.
 *
 * Usage:
 * Sign-on with OpenID is a two step process:
 * Step one is authentication with the provider:
 * <code>
 * $openid = new LightOpenID;
 * $openid->identity = 'ID supplied by user';
 * header('Location: ' . $openid->authUrl());
 * </code>
 * The provider then sends various parameters via GET, one of them is openid_mode.
 * Step two is verification:
 * <code>
 * if($_GET['openid_mode']) {
 *	 $openid = new LightOpenID;
 *	 echo $openid->validate() ? 'Logged in.' : 'Failed';
 * }
 * </code>
 *
 * Optionally, you can set $returnUrl and $realm (or $trustRoot, which is an alias).
 * The default values for those are:
 * $openid->realm	 = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
 * $openid->returnUrl = $openid->realm . $_SERVER['REQUEST_URI'];
 * If you don't know their meaning, refer to any openid tutorial, or specification. Or just guess.
 *
 * AX and SREG extensions are supported.
 * To use them, specify $openid->required and/or $openid->optional.
 * These are arrays, with values being AX schema paths (the 'path' part of the URL).
 * For example:
 *   $openid->required = array('namePerson/friendly', 'contact/email');
 *   $openid->optional = array('namePerson/first');
 * If the server supports only SREG or OpenID 1.1, these are automaticaly
 * mapped to SREG names, so that user doesn't have to know anything about the server.
 *
 * To get the values, use $openid->getAttributes().
 *
 *
 * The library depends on curl, and requires PHP 5.
 * @author Mewp
 * @copyright Copyright (c) 2010, Mewp
 * @license http://www.opensource.org/licenses/mit-license.php MIT
 */
class LightOpenID
{
	public $returnUrl
		 , $required = array()
		 , $optional = array();
	private $identity;
	protected $server, $version, $trustRoot, $aliases, $identifier_select = false;
	static protected $ax_to_sreg = array(
		'namePerson/friendly'	 	=> 'nickname',
		'contact/email'		   	=> 'email',
		'namePerson/first'			=> 'firstname',
		'namePerson/last'		 	=> 'lastname',
		'namePerson'			  	=> 'fullname',
		'birthDate'			   	=> 'dob',
		'person/gender'		   	=> 'gender',
		'contact/postalCode/home' 	=> 'postcode',
		'contact/country/home'		=> 'country',
		'pref/language'		   	=> 'language',
		'pref/timezone'		   	=> 'timezone',
		'media/image/default'		=> 'avatar',
		);

	function __construct()
	{
		$this->trustRoot = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
//		$this->trustRoot = 'http://' . $_SERVER['HTTP_HOST'];

		$this->returnUrl = $this->trustRoot . $_SERVER['REQUEST_URI'];

		if (!function_exists('curl_exec')) {
			throw new ErrorException('Curl extension is required.');
		}
	}

	function __set($name, $value)
	{
		switch($name) {
		case 'identity':
			if(stripos($value, 'http') === false && $value) $value = 'http://' . $value;
			$this->$name = $value;
			break;
		case 'trustRoot':
		case 'realm':
			$this->trustRoot = $value;
		}
	}

	function __get($name)
	{
		switch($name) {
		case 'identity':
			return $this->$name;
		case 'trustRoot':
		case 'realm':
			return $this->trustRoot;
		}
	}

	protected function request($url, $method='GET', $params=array(), $update_claimed_id=false)
	{
		if (function_exists('curl_init')
			&& (!in_array('https', stream_get_wrappers()) || !ini_get('safe_mode') && !ini_get('open_basedir'))
		) {
			return $this->request_curl($url, $method, $params, $update_claimed_id);
		}
		return $this->request_streams($url, $method, $params, $update_claimed_id);
	}

	function hostExists($url)
	{
		if (strpos($url, '/') === false) {
			$server = $url;
		} else {
			$server = @parse_url($url, PHP_URL_HOST);
		}

		if (!$server) {
			return false;
		}

		return !!gethostbynamel($server);
	}

	protected function request_streams($url, $method='GET', $params=array(), $update_claimed_id)
	{
		if(!$this->hostExists($url)) {
			throw new ErrorException("Could not connect to $url.", 404);
		}

		$params = http_build_query($params, '', '&');
		switch($method) {
		case 'GET':
			$opts = array(
				'http' => array(
					'method' => 'GET',
					'header' => 'Accept: application/xrds+xml, */*',
					'ignore_errors' => true,
				), 'ssl' => array(
					'CN_match' => parse_url($url, PHP_URL_HOST),
				),
			);
			$url = $url . ($params ? '?' . $params : '');
			break;
		case 'POST':
			$opts = array(
				'http' => array(
					'method' => 'POST',
					'header'  => 'Content-type: application/x-www-form-urlencoded',
					'content' => $params,
					'ignore_errors' => true,
				), 'ssl' => array(
					'CN_match' => parse_url($url, PHP_URL_HOST),
				),
			);
			break;
		case 'HEAD':
			# We want to send a HEAD request,
			# but since get_headers doesn't accept $context parameter,
			# we have to change the defaults.
			$default = stream_context_get_options(stream_context_get_default());
			stream_context_get_default(
				array(
					'http' => array(
						'method' => 'HEAD',
						'header' => 'Accept: application/xrds+xml, */*',
						'ignore_errors' => true,
					), 'ssl' => array(
						'CN_match' => parse_url($url, PHP_URL_HOST),
					),
				)
			);

			$url = $url . ($params ? '?' . $params : '');
			$headers = get_headers ($url);
			if(!$headers) {
				return array();
			}

			if(intval(substr($headers[0], strlen('HTTP/1.1 '))) == 405) {
				# The server doesn't support HEAD, so let's emulate it with
				# a GET.
				$args = func_get_args();
				$args[1] = 'GET';
				call_user_func_array(array($this, 'request_streams'), $args);
				return $this->headers;
			}

			$headers = $this->parse_header_array($headers, $update_claimed_id);

			# And restore them.
			stream_context_get_default($default);
			return $headers;
		}

		if($this->verify_peer) {
			$opts['ssl'] += array(
				'verify_peer' => true,
				'capath'	  => $this->capath,
				'cafile'	  => $this->cainfo,
			);
		}

		$context = stream_context_create ($opts);
		$data = file_get_contents($url, false, $context);
		# This is a hack for providers who don't support HEAD requests.
		# It just creates the headers array for the last request in $this->headers.
		if(isset($http_response_header)) {
			$this->headers = $this->parse_header_array($http_response_header, $update_claimed_id);
		}

		return $data;
	}

	protected function parse_header_array($array, $update_claimed_id)
	{
		$headers = array();
		foreach($array as $header) {
			$pos = strpos($header,':');
			if ($pos !== false) {
				$name = strtolower(trim(substr($header, 0, $pos)));
				$headers[$name] = trim(substr($header, $pos+1));

				# Following possible redirections. The point is just to have
				# claimed_id change with them, because the redirections
				# are followed automatically.
				# We ignore redirections with relative paths.
				# If any known provider uses them, file a bug report.
				if($name == 'location' && $update_claimed_id) {
					if(strpos($headers[$name], 'http') === 0) {
						$this->identity = $this->claimed_id = $headers[$name];
					} elseif($headers[$name][0] == '/') {
						$parsed_url = parse_url($this->claimed_id);
						$this->identity =
						$this->claimed_id = $parsed_url['scheme'] . '://'
										  . $parsed_url['host']
										  . $headers[$name];
					}
				}
			}
		}
		return $headers;
	}

	protected function request_curl($url, $method='GET', $params=array(), $update_claimed_id)
	{
		$params = http_build_query($params, '', '&');
		$curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : ''));
		curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
		curl_setopt($curl, CURLOPT_HEADER, false);
		curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*'));

		if($this->verify_peer !== null) {
			curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer);
			if($this->capath) {
				curl_setopt($curl, CURLOPT_CAPATH, $this->capath);
			}

			if($this->cainfo) {
				curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo);
			}
		}

		if ($method == 'POST') {
			curl_setopt($curl, CURLOPT_POST, true);
			curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
		} elseif ($method == 'HEAD') {
			curl_setopt($curl, CURLOPT_HEADER, true);
			curl_setopt($curl, CURLOPT_NOBODY, true);
		} else {
			curl_setopt($curl, CURLOPT_HEADER, true);
			curl_setopt($curl, CURLOPT_HTTPGET, true);
		}
		$response = curl_exec($curl);

		if($method == 'HEAD' && curl_getinfo($curl, CURLINFO_HTTP_CODE) == 405) {
			curl_setopt($curl, CURLOPT_HTTPGET, true);
			$response = curl_exec($curl);
			$response = substr($response, 0, strpos($response, "\r\n\r\n"));
		}

		if($method == 'HEAD' || $method == 'GET') {
			$header_response = $response;

			# If it's a GET request, we want to only parse the header part.
			if($method == 'GET') {
				$header_response = substr($response, 0, strpos($response, "\r\n\r\n"));
			}

			$headers = array();
			foreach(explode("\n", $header_response) as $header) {
				$pos = strpos($header,':');
				if ($pos !== false) {
					$name = strtolower(trim(substr($header, 0, $pos)));
					$headers[$name] = trim(substr($header, $pos+1));
				}
			}

			if($update_claimed_id) {
				# Updating claimed_id in case of redirections.
				$effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
				if($effective_url != $url) {
					$this->identity = $this->claimed_id = $effective_url;
				}
			}

			if($method == 'HEAD') {
				return $headers;
			} else {
				$this->headers = $headers;
			}
		}

		if (curl_errno($curl)) {
			throw new ErrorException(curl_error($curl), curl_errno($curl));
		}

		return $response;
	}

	protected function build_url($url, $parts)
	{
		if(isset($url['query'], $parts['query'])) {
			$parts['query'] = $url['query'] . '&' . $parts['query'];
		}

		$url = $parts + $url;
		$url = $url['scheme'] . '://'
			 . (empty($url['username'])?''
				 :(empty($url['password'])? "{$url['username']}@"
				 :"{$url['username']}:{$url['password']}@"))
			 . $url['host']
			 . (empty($url['port'])?'':":{$url['port']}")
			 . (empty($url['path'])?'':$url['path'])
			 . (empty($url['query'])?'':"?{$url['query']}")
			 . (empty($url['fragment'])?'':":{$url['fragment']}");
		return $url;
	}

	protected function htmlTag($content, $tag, $attrName, $attrValue, $valueName)
	{
		preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1);
		preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1);
		preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2);

		$result = array_merge($matches1[1], $matches2[1]);
		return empty($result)?false:$result[0];
	}

	/**
	 * Performs Yadis and HTML discovery. Normally not used.
	 * @param $url Identity URL.
	 * @return String OP Endpoint (i.e. OpenID provider address).
	 * @throws ErrorException
	 */
	function discover($url)
	{
		if (!$url) throw new ErrorException('No identity supplied.');
		# Use xri.net proxy to resolve i-name identities
		if (!preg_match('#^https?:#', $url)) {
			$url = "https://xri.net/$url";
		}

		# We save the original url in case of Yadis discovery failure.
		# It can happen when we'll be lead to an XRDS document
		# which does not have any OpenID2 services.
		$originalUrl = $url;

		# A flag to disable yadis discovery in case of failure in headers.
		$yadis = true;

		# We'll jump a maximum of 5 times, to avoid endless redirections.
		for ($i = 0; $i < 5; $i ++) {
			if ($yadis) {
				$headers = $this->request($url, 'HEAD', array(), true);

				$next = false;
				if (isset($headers['x-xrds-location'])) {
					$url = $this->build_url(parse_url($url), parse_url(trim($headers['x-xrds-location'])));
					$next = true;
				}

				if (isset($headers['content-type'])
					&& (strpos($headers['content-type'], 'application/xrds+xml') !== false
						|| strpos($headers['content-type'], 'text/xml') !== false)
				) {
					# Apparently, some providers return XRDS documents as text/html.
					# While it is against the spec, allowing this here shouldn't break
					# compatibility with anything.
					# ---
					# Found an XRDS document, now let's find the server, and optionally delegate.
					$content = $this->request($url, 'GET');

					preg_match_all('#<Service.*?>(.*?)</Service>#s', $content, $m);
					foreach($m[1] as $content) {
						$content = ' ' . $content; # The space is added, so that strpos doesn't return 0.

						# OpenID 2
						$ns = preg_quote('http://specs.openid.net/auth/2.0/', '#');
						if(preg_match('#<Type>\s*'.$ns.'(server|signon)\s*</Type>#s', $content, $type)) {
							if ($type[1] == 'server') $this->identifier_select = true;

							preg_match('#<URI.*?>(.*)</URI>#', $content, $server);
							preg_match('#<(Local|Canonical)ID>(.*)</\1ID>#', $content, $delegate);
							if (empty($server)) {
								return false;
							}
							# Does the server advertise support for either AX or SREG?
							$this->ax   = (bool) strpos($content, '<Type>http://openid.net/srv/ax/1.0</Type>');
							$this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>')
									   || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>');

							$server = $server[1];
							if (isset($delegate[2])) $this->identity = trim($delegate[2]);
							$this->version = 2;

							$this->server = $server;
							return $server;
						}

						# OpenID 1.1
						$ns = preg_quote('http://openid.net/signon/1.1', '#');
						if (preg_match('#<Type>\s*'.$ns.'\s*</Type>#s', $content)) {

							preg_match('#<URI.*?>(.*)</URI>#', $content, $server);
							preg_match('#<.*?Delegate>(.*)</.*?Delegate>#', $content, $delegate);
							if (empty($server)) {
								return false;
							}
							# AX can be used only with OpenID 2.0, so checking only SREG
							$this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>')
									   || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>');

							$server = $server[1];
							if (isset($delegate[1])) $this->identity = $delegate[1];
							$this->version = 1;

							$this->server = $server;
							return $server;
						}
					}

					$next = true;
					$yadis = false;
					$url = $originalUrl;
					$content = null;
					break;
				}
				if ($next) continue;

				# There are no relevant information in headers, so we search the body.
				$content = $this->request($url, 'GET', array(), true);

				if (isset($this->headers['x-xrds-location'])) {
					$url = $this->build_url(parse_url($url), parse_url(trim($this->headers['x-xrds-location'])));
					continue;
				}

				$location = $this->htmlTag($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content');
				if ($location) {
					$url = $this->build_url(parse_url($url), parse_url($location));
					continue;
				}
			}

			if (!$content) $content = $this->request($url, 'GET');

			# At this point, the YADIS Discovery has failed, so we'll switch
			# to openid2 HTML discovery, then fallback to openid 1.1 discovery.
			$server   = $this->htmlTag($content, 'link', 'rel', 'openid2.provider', 'href');
			$delegate = $this->htmlTag($content, 'link', 'rel', 'openid2.local_id', 'href');
			$this->version = 2;

			if (!$server) {
				# The same with openid 1.1
				$server   = $this->htmlTag($content, 'link', 'rel', 'openid.server', 'href');
				$delegate = $this->htmlTag($content, 'link', 'rel', 'openid.delegate', 'href');
				$this->version = 1;
			}

			if ($server) {
				# We found an OpenID2 OP Endpoint
				if ($delegate) {
					# We have also found an OP-Local ID.
					$this->identity = $delegate;
				}
				$this->server = $server;
				return $server;
			}

			throw new ErrorException("No OpenID Server found at $url", 404);
		}
		throw new ErrorException('Endless redirection!', 500);
	}

	protected function sregParams()
	{
		$params = array();
		if($this->required) {
			$params['openid.sreg.required'] = array();
			foreach($this->required as $required) {
				if(!isset(self::$ax_to_sreg[$required])) continue;
				$params['openid.sreg.required'][] = self::$ax_to_sreg[$required];
			}
			$params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']);
		}

		if($this->optional) {
			$params['openid.sreg.optional'] = array();
			foreach($this->optional as $optional) {
				if(!isset(self::$ax_to_sreg[$optional])) continue;
				$params['openid.sreg.optional'][] = self::$ax_to_sreg[$optional];
			}
			$params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']);
		}
		return $params;
	}
	protected function axParams()
	{
		$params = array();
		if($this->required || $this->optional) {
			$params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0';
			$params['openid.ax.mode'] = 'fetch_request';
			$this->aliases  = array();
			$counts   = array();
			$required = array();
			$optional = array();
			foreach(array('required','optional') as $type) {
				foreach($this->$type as $alias => $field) {
					if(is_int($alias)) $alias = strtr($field, '/', '_');
					$this->aliases[$alias] = 'http://axschema.org/' . $field;
					if(empty($counts[$alias])) $counts[$alias] = 0;
					$counts[$alias] += 1;
					${$type}[] = $alias;
				}
			}
			foreach($this->aliases as $alias => $ns) {
				$params['openid.ax.type.' . $alias] = $ns;			}
			foreach($counts as $alias => $count) {
				if($count == 1) continue;
				$params['openid.ax.count.' . $alias] = $count;
			}
			$params['openid.ax.required'] = implode(',', $required);
			$params['openid.ax.if_avaiable'] = implode(',', $optional);
		}
		return $params;
	}

	protected function authUrl_v1()
	{
		$params = array(
			'openid.return_to'  => $this->returnUrl,
			'openid.mode'	   => 'checkid_setup',
			'openid.identity'   => $this->identity,
			'openid.trust_root' => $this->trustRoot,
			) + $this->sregParams();

		return $this->build_url(parse_url($this->server)
							   , array('query' => http_build_query($params)));
	}

	protected function authUrl_v2($identifier_select)
	{
		$params = array(
			'openid.ns'		  => 'http://specs.openid.net/auth/2.0',
			'openid.mode'		=> 'checkid_setup',
			'openid.return_to'   => $this->returnUrl,
			'openid.realm'	   => $this->trustRoot,
		);
		if($this->ax) {
			$params += $this->axParams();
		} if($this->sreg) {
			$params += $this->sregParams();
		} else {
			# If OP doesn't advertise either SREG, nor AX, let's send them both
			# in worst case we don't get anything in return.
			$params += $this->axParams() + $this->sregParams();
		}
		if($identifier_select) {
			$params['openid.identity'] = $params['openid.claimed_id']
				 = 'http://specs.openid.net/auth/2.0/identifier_select';
		} else {
			$params['openid.identity'] = $params['openid.claimed_id'] = $this->identity;
		}

		return $this->build_url(parse_url($this->server)
							   , array('query' => http_build_query($params)));
	}

	/**
	 * Returns authentication url. Usually, you want to redirect your user to it.
	 * @return String The authentication url.
	 * @param String $select_identifier Whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1.
	 * @throws ErrorException
	 */
	function authUrl($identifier_select = null)
	{
		if(!$this->server) $this->Discover($this->identity);

		if($this->version == 2) {
			if($identifier_select === null) {
				return $this->authUrl_v2($this->identifier_select);
			}
			return $this->authUrl_v2($identifier_select);
		}
		return $this->authUrl_v1();
	}

	/**
	 * Performs OpenID verification with the OP.
	 * @return Bool Whether the verification was successful.
	 * @throws ErrorException
	 */
	function validate()
	{
		$params = array(
			'openid.assoc_handle' => $_GET['openid_assoc_handle'],
			'openid.signed'	   => $_GET['openid_signed'],
			'openid.sig'		  => $_GET['openid_sig'],
			);

		if(isset($_GET['openid_op_endpoint'])) {
			# We're dealing with an OpenID 2.0 server, so let's set an ns
			# Even though we should know location of the endpoint,
			# we still need to verify it by discovery, so $server is not set here
			$params['openid.ns'] = 'http://specs.openid.net/auth/2.0';
		}
		$server = $this->discover($_GET['openid_identity']);

		foreach(explode(',', $_GET['openid_signed']) as $item) {
			$params['openid.' . $item] = $_GET['openid_' . str_replace('.','_',$item)];
		}

		$params['openid.mode'] = 'check_authentication';

		$response = $this->request($server, 'POST', $params);

		return preg_match('/is_valid\s*:\s*true/i', $response);
	}
	protected function getAxAttributes()
	{
		$alias = null;
		if (isset($_GET['openid_ns_ax'])
			&& $_GET['openid_ns_ax'] != 'http://openid.net/srv/ax/1.0'
		) { # It's the most likely case, so we'll check it before
			$alias = 'ax';
		} else {
			# 'ax' prefix is either undefined, or points to another extension,
			# so we search for another prefix
			foreach($_GET as $key => $val) {
				if (substr($key, 0, strlen('openid_ns_')) == 'openid_ns_'
					&& $val == 'http://openid.net/srv/ax/1.0'
				) {
					$alias = substr($key, strlen('openid_ns_'));
					break;
				}
			}
		}
		if (!$alias) {
			# An alias for AX schema has not been found,
			# so there is no AX data in the OP's response
			return array();
		}

		foreach ($_GET as $key => $value) {
			$keyMatch = 'openid_' . $alias . '_value_';
			if(substr($key, 0, strlen($keyMatch)) != $keyMatch) {
				continue;
			}
			$key = substr($key, strlen($keyMatch));
			if(!isset($_GET['openid_' . $alias . '_type_' . $key])) {
				# OP is breaking the spec by returning a field without
				# associated ns. This shouldn't happen, but it's better
				# to check, than cause an E_NOTICE.
				continue;
			}
			$key = substr($_GET['openid_' . $alias . '_type_' . $key],
						  strlen('http://axschema.org/'));
			$attributes[$key] = $value;
		}
		# Found the AX attributes, so no need to scan for SREG.
		return $attributes;
	}
	protected function getSregAttributes()
	{
		$attributes = array();
		$sreg_to_ax = array_flip(self::$ax_to_sreg);
		foreach ($_GET as $key => $value) {
			$keyMatch = 'openid_sreg_';
			if(substr($key, 0, strlen($keyMatch)) != $keyMatch) {
				continue;
			}
			$key = substr($key, strlen($keyMatch));
			if(!isset($sreg_to_ax[$key])) {
				# The field name isn't part of the SREG spec, so we ignore it.
				continue;
			}
			$attributes[$sreg_to_ax[$key]] = $value;
		}
		return $attributes;
	}
	/**
	 * Gets AX/SREG attributes provided by OP. should be used only after successful validaton.
	 * Note that it does not guarantee that any of the required/optional parameters will be present,
	 * or that there will be no other attributes besides those specified.
	 * In other words. OP may provide whatever information it wants to.
	 *	 * SREG names will be mapped to AX names.
	 *	 * @return Array Array of attributes with keys being the AX schema names, e.g. 'contact/email'
	 * @see http://www.axschema.org/types/
	 */
	function getAttributes()
	{
		$attributes;
		if (isset($_GET['openid_ns'])
			&& $_GET['openid_ns'] == 'http://specs.openid.net/auth/2.0'
		) { # OpenID 2.0
			# We search for both AX and SREG attributes, with AX taking precedence.
			return $this->getAxAttributes() + $this->getSregAttributes();
		}
		return $this->getSregAttributes();
	}
}
